﻿using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;

namespace LinkDownloader.Core
{
    public class DownloadJob
    {

        public delegate void ChangedEventHandler(ChangedEventArgs e);
        public delegate void RequestEventHandler(out string username, out string password);
        public delegate void ProgressChangedEventHandler(int percent);

        public event ChangedEventHandler Started;
        public event ChangedEventHandler Completed;
        public event ChangedEventHandler Cancelled;        
        public event ChangedEventHandler Error;
        public event ChangedEventHandler Skipped;
        public event ChangedEventHandler StatusChanged;
        public event ChangedEventHandler CompletedSizeChanged;
        public event ChangedEventHandler TotalSizeChanged;
        public event ChangedEventHandler TransferRateChanged;
        public event ChangedEventHandler FileChanged;
        public event RequestEventHandler AuthenticationRequested;
        public event ProgressChangedEventHandler ProgressChanged;

        private Downloader downloader = null;

        private TimeSpan counter = TimeSpan.Zero;

        private int id = -1;
        public int ID
        {
            get { return this.id; }
        }

        private string link = string.Empty;
        public string Link
        {
            get { return this.link; }
            set { this.link = value; }
        }

        private string username = string.Empty;
        public string Username
        {
            get { return this.username; }
            set { this.username = value; }
        }

        private string password = string.Empty;
        public string Password
        {
            get { return this.password; }
            set { this.password = value; }
        }

        private bool skipOnError = false;
        public bool SkipOnError
        {
            get { return this.skipOnError; }
            set { this.skipOnError = value; }
        }

        private string fileDirectory = string.Empty;
        public string FileDirectory
        {
            get { return this.fileDirectory; }
            set { this.fileDirectory = value; }
        }

        private BackgroundWorker worker = null;
        public BackgroundWorker Worker
        {
            get { return this.worker; }
        }

        private string fileName = string.Empty;
        public string FileName
        {
            get { return this.fileName; }
            set { this.fileName = value; }
        }

        private long completedSize = 0;
        public long CompletedSize
        {
            get { return this.completedSize; }
        }

        private long totalSize = 0;
        public long TotalSize
        {
            get { return this.totalSize; }
        }

        private string message = string.Empty;
        public string Message
        {
            get { return this.message; }
            set { this.message = value; }
        }

        private int progress = 0;
        public int Progress
        {
            get { return this.progress; }
        }

        private TimeSpan elapsed = TimeSpan.Zero;
        public TimeSpan Elapsed
        {
            get { return this.elapsed; }
        }

        private TimeSpan timeleft = TimeSpan.MaxValue;
        public TimeSpan TimeLeft
        {
            get { return this.timeleft; }
        }

        private double transferRate = 0;
        public double TransferRate
        {
            get { return this.transferRate; }
        }

        private DownloadStatus status = DownloadStatus.None;
        public DownloadStatus Status
        {
            get { return this.status; }
        }
        
        public DownloadJob(Downloader downloader, int id)
        {

            this.downloader = downloader;
            this.id = id;

            this.worker = new BackgroundWorker();
            this.worker.WorkerReportsProgress = true;
            this.worker.WorkerSupportsCancellation = true;

            this.worker.DoWork += (BackgroundWorker_DoWork);
            this.worker.ProgressChanged += (BackgroundWorker_ProgressChanged);
            this.worker.RunWorkerCompleted += (BackgroundWorker_RunWorkerCompleted);

        }

        protected virtual void OnStarted(ChangedEventArgs e)
        {
            if (this.Started != null) { this.Started(e); }
        }

        protected virtual void OnCompleted(ChangedEventArgs e)
        {
            if (this.Completed != null) { this.Completed(e); }
        }

        protected virtual void OnCancelled(ChangedEventArgs e)
        {
            if (this.Cancelled != null) { this.Cancelled(e); }
        }

        protected virtual void OnError(ChangedEventArgs e)
        {
            if (this.Error != null) { this.Error(e); }
        }

        protected virtual void OnStatusChanged(ChangedEventArgs e)
        {
            if (this.StatusChanged != null) { this.StatusChanged(e); }
        }

        protected virtual void OnSkipped(ChangedEventArgs e)
        {
            if (this.Skipped != null) { this.Skipped(e); }
        }

        protected virtual void OnCompletedSizeChanged(ChangedEventArgs e)
        {
            
            if (this.CompletedSizeChanged != null) { this.CompletedSizeChanged(e); }
        }

        protected virtual void OnTotalSizeChanged(ChangedEventArgs e)
        {
            if (this.TotalSizeChanged != null) { this.TotalSizeChanged(e); }
        }

        protected virtual void OnTransferRateChanged(ChangedEventArgs e)
        {
            if (this.TransferRateChanged != null) { this.TransferRateChanged(e); }
        }

        protected virtual void OnFileChanged(ChangedEventArgs e)
        {
            if (this.FileChanged != null) { this.FileChanged(e); }
        }

        protected virtual void OnAuthenticationRequested(out string username, out string password)
        {
            if (this.AuthenticationRequested != null)
            {
                this.AuthenticationRequested(out username, out password);
            }
            else
            {
                username = this.username;
                password = this.password;
            }
        }

        protected virtual void OnProgressChanged(int percent)
        {
            this.progress += percent;
            if (this.progress > 100) { this.progress = 100; }
            if (this.ProgressChanged != null) { this.ProgressChanged(percent); }
        }

        public virtual void StartDownload()
        {
            this.status = DownloadStatus.Connecting;
            this.worker.RunWorkerAsync(this);
            this.OnStarted(new ChangedEventArgs() { Message = "Started download!" });
            this.OnStatusChanged(new ChangedEventArgs() { Message = this.message });
        }

        public virtual void PauseDownload()
        {
            //this.status = DownloadStatus.Connecting;
            //this.worker.RunWorkerAsync(this);
            //this.OnStarted(new ChangedEventArgs() { Message = "Started download!" });
            //this.OnStatusChanged(new ChangedEventArgs() { Message = this.message });
        }

        public virtual void CancelDownload()
        {
            this.status = DownloadStatus.Cancelled;
            this.worker.CancelAsync();
            this.OnCancelled(new ChangedEventArgs() { Message = "Download cancelled!" });
            this.OnStatusChanged(new ChangedEventArgs() { Message = this.message });
        }

        private static void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {

            DownloadJob param = e.Result as DownloadJob;
            ChangedEventArgs e1 = new ChangedEventArgs();
            DownloadStatus s = param.status;

            switch (s)
            {

                case DownloadStatus.Completed:
                    e1.Message = param.message;
                    param.OnCompleted(e1);
                    param.downloader.OnJobCompleted(param.id, e1);
                    break;

                case DownloadStatus.Skipped:
                    e1.Message = param.message;
                    param.OnSkipped(e1);
                    param.downloader.OnJobSkipped(param.id, e1);
                    break;

                case DownloadStatus.Cancelled:
                    e1.Message = param.message;
                    param.OnCancelled(e1);
                    param.downloader.OnJobCancelled(param.id, e1);
                    break;

            }

        }

        private static void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {

            DownloadJob param = e.UserState as DownloadJob;
            DownloadStatus s = (DownloadStatus)e.ProgressPercentage;
            ChangedEventArgs e1 = new ChangedEventArgs();

            switch (s)
            {

                case DownloadStatus.Error:
                    e1.Message = param.message;
                    param.OnError(e1);
                    param.downloader.OnJobDownloadError(param.id, e1);
                    break;

                case DownloadStatus.Downloading:
                    int prg = (int)((param.completedSize * 100) / param.totalSize) - param.progress;
                    param.OnProgressChanged(prg);
                    param.downloader.OnJobProgressChanged(param.id, prg);
                    break;
                    
                case DownloadStatus.Connecting:
                case DownloadStatus.Redirecting:
                    param.OnStatusChanged(new ChangedEventArgs() { Message = param.message });
                    break;

            }

        }

        private static void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            DownloadJob param = e.Argument as DownloadJob;
            if (param != null)
            {
                e.Result = DownloadAndReport(param);
            }
        }

        private static DownloadJob DownloadAndReport(DownloadJob param)
        {

            param.message = param.link;
            param.status = DownloadStatus.Connecting;
            param.OnStatusChanged(new ChangedEventArgs() { Message = param.message });
            param.worker.ReportProgress((int)param.status, param);

            Uri url = new Uri(param.Link.Trim());
            HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest;

            try
            {
                req.AllowAutoRedirect = true;
                if (!string.IsNullOrEmpty(param.Username))
                {
                    byte[] authBytes = Encoding.UTF8.GetBytes((param.username + ":" + param.password).ToCharArray());
                    req.Headers["Authorization"] = "Basic " + Convert.ToBase64String(authBytes);
                }
                req.MaximumAutomaticRedirections = 100;
                req.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)";
                req.Accept = "*/*";
                req.KeepAlive = true;
                req.Timeout = int.MaxValue; //prefRequestTimeout
                req.CookieContainer = GetUriCookieContainer(url.AbsoluteUri);
            }
            catch
            {
                param.message = "Error downloading local file";
                param.status = DownloadStatus.Error;
                param.worker.ReportProgress((int)param.status, param);
                return param;
            }


            // Get the stream from the returned web response
            HttpWebResponse webresponse = null;
            try
            {
                webresponse = (HttpWebResponse)req.GetResponse();
                if (webresponse.ResponseUri.AbsoluteUri != url.AbsoluteUri)
                {
                    param.link = webresponse.ResponseUri.AbsoluteUri;
                    param.message = param.link;
                    param.status = DownloadStatus.Redirecting;
                    param.OnStatusChanged(new ChangedEventArgs() { Message = param.message });
                    param.worker.ReportProgress((int)param.status, param);
                    DownloadAndReport(param);
                    return param;
                }
                param.message = param.link;
                param.worker.ReportProgress((int)param.status, param);

            }
            catch (WebException we)
            {
                param.message = we.ToString();
                param.status = DownloadStatus.Error;
                param.worker.ReportProgress((int)param.status, param);
                return param;
            }

            try
            {

                string file = Path.GetFileName(webresponse.ResponseUri.AbsoluteUri);
                if (!webresponse.ContentType.Contains("text/html"))
                {
                    file = file.Replace(".html", "");
                }
                else
                {
                    param.message = "Check the Username or Password!";
                    if (param.skipOnError)
                    {
                        param.status = DownloadStatus.Skipped;
                        param.worker.ReportProgress((int)param.status, param);
                        return param;
                    }
                    else
                    {
                        param.OnAuthenticationRequested(out param.username, out param.password);
                        DownloadAndReport(param);
                    }
                }
                param.fileName = file;
                param.totalSize = webresponse.ContentLength / 1024; // bytes to KB
                param.message = string.Format("{0:0.00} KB", param.totalSize);
                param.status = DownloadStatus.Downloading;
                param.OnTotalSizeChanged(new ChangedEventArgs() { Argument = param.totalSize, Message = param.message });
                param.OnFileChanged(new ChangedEventArgs() { Argument = param.fileName });
                param.worker.ReportProgress((int)param.status, param);

                string filepath = param.FileDirectory + ((param.FileDirectory.EndsWith("\\")) ? file : "\\" + file);
                FileStream writeStream = new FileStream(filepath, FileMode.Create, FileAccess.ReadWrite);
                Stream readStream = webresponse.GetResponseStream();
                try
                {
                    DateTime dt = DateTime.Now;
                    const int length = 256;
                    Byte[] buffer = new Byte[length];
                    int bytesRead = readStream.Read(buffer, 0, length);
                    // write the required bytes
                    while (bytesRead > 0)
                    {
                        if (param.Worker.CancellationPending) { return param; }
                        writeStream.Write(buffer, 0, bytesRead);
                        bytesRead = readStream.Read(buffer, 0, length);
                        param.completedSize = writeStream.Length / 1024;  // bytes to KB
                        //param.progress = (int)((param.completedSize * 100) / param.totalSize);
                        param.status = DownloadStatus.Downloading;
                        dt = param.UpdateAndGetElaspsedTime(dt, bytesRead);
                        param.OnCompletedSizeChanged(new ChangedEventArgs() { Argument = param.completedSize, Message = param.message });
                        param.Worker.ReportProgress((int)param.status, param);
                    }
                    readStream.Close();
                    writeStream.Close();

                }
                catch (Exception exception)
                {
                    param.message = exception.ToString();
                    param.status = DownloadStatus.Error;
                    param.worker.ReportProgress((int)param.status, param);
                    return param;
                }
                param.message = file;
                param.status = DownloadStatus.Completed;
                param.worker.ReportProgress((int)param.status, param);

            }
            catch (Exception exception)
            {
                param.message = exception.ToString();
                param.status = DownloadStatus.Error;
                param.worker.ReportProgress((int)param.status, param);
                return param;
            }
            return param;

        }

        private DateTime UpdateAndGetElaspsedTime(DateTime start, int length)
        {

            DateTime dt = DateTime.Now;
            TimeSpan t = dt.Subtract(start);
            this.elapsed += t;
            this.transferRate = length / t.TotalSeconds / 1024; // bytes to KB (KB/s)
            double s = (this.totalSize - this.completedSize) / this.transferRate;
            this.timeleft = new TimeSpan(0, 0, (int)s);
            
            this.counter += t;
            s = this.counter.TotalSeconds;
            if (s > 1)
            {
                this.counter = TimeSpan.Zero;
                this.OnTransferRateChanged(new ChangedEventArgs() { Argument = s });
            }

            return dt;
        }

        private static CookieContainer GetUriCookieContainer(string uri)
        {
            CookieContainer cookies = new CookieContainer();
            foreach (Cookie c in CookieManager.GetCookie(uri))
            {
                cookies.SetCookies(new Uri(uri), c.ToString());
            }
            return cookies;
        }

    }

}
